package com.gitee.wsl.compose.ui.richtext.edit.state


import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.gitee.wsl.compose.ui.richtext.edit.annotation.ExperimentalRichTextApi
import com.gitee.wsl.compose.ui.richtext.edit.append
import com.gitee.wsl.compose.ui.richtext.edit.model.span.RichSpan
import com.gitee.wsl.compose.ui.richtext.edit.model.span.style.RichSpanStyle
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.RichParagraph
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.api.ConfigurableListLevel
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.DefaultParagraphType
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.list.OrderedList
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.ParagraphType
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.list.UnorderedList
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.startText
import com.gitee.wsl.ext.list.removeRange
import kotlin.math.max

internal fun RichTextState.resetParagraphType(paragraph: RichParagraph) {
    updateParagraphType(paragraph, DefaultParagraphType())
}


/**
 * Merges two [RichParagraph]s into one.
 * The [firstParagraph] will be modified, containing the text of both [firstParagraph] and [secondParagraph].
 * And the [secondParagraph] will be removed.
 *
 * @param firstParagraph The first [RichParagraph].
 * @param secondParagraph The second [RichParagraph].
 */
internal fun RichTextState.mergeTwoRichParagraphs(
    firstParagraph: RichParagraph,
    secondParagraph: RichParagraph,
) {
    // Update the children paragraph of the second paragraph to the first paragraph.
    secondParagraph.updateChildrenParagraph(firstParagraph)

    // Add the children of the second paragraph to the first paragraph.
    firstParagraph.children.addAll(secondParagraph.children)

    // Remove the second paragraph from the rich paragraph list.
    richParagraphList.remove(secondParagraph)
}

/**
 * Inserts the given [newParagraphs] at the specified [position] in the [RichTextState].
 *
 * The insertion behavior depends on the paragraphs and the insertion position:
 * 1. If the list contains a single paragraph:
 *    - The content is inserted at the exact position within the existing paragraph
 *    - All styles (both span and rich span styles) are preserved
 * 2. If the list contains multiple paragraphs:
 *    - The current paragraph is split at the insertion point
 *    - The new paragraphs are inserted between the split parts
 *    - All styles from the original paragraph are preserved in both split parts
 *
 * Special cases:
 * - If position is 0, the content is inserted at the start
 * - If position equals text length, the content is appended at the end
 * - If the list is empty, no changes are made
 *
 * @param newParagraphs The new paragraphs to insert.
 * @param position The position at which to insert the new paragraphs.
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.insertParagraphs(
    newParagraphs: List<RichParagraph>,
    position: Int
) {
    val position = position.coerceIn(0, annotatedString.text.length)

    if (newParagraphs.isEmpty())
        return

    if (richParagraphList.isEmpty())
        richParagraphList.add(RichParagraph())

    richParagraphList.first().let { p ->
        if (p.children.isEmpty())
            p.children.add(RichSpan(paragraph = p))
    }

    val firstNewParagraph = newParagraphs.first()

    val richSpan = getRichSpanByTextIndex(
        textIndex = position - 1,
        ignoreCustomFiltering = true,
    ) ?: return

    val targetParagraph = richSpan.paragraph
    val paragraphIndex = richParagraphList.indexOf(targetParagraph)

    val sliceIndex = max(position, richSpan.textRange.min)

    val targetParagraphFirstHalf = targetParagraph
    val targetParagraphSecondHalf = targetParagraph.slice(
        richSpan = richSpan,
        startIndex = sliceIndex,
        removeSliceIndex = false,
    )

    if (targetParagraphFirstHalf.isEmpty() && firstNewParagraph.isNotEmpty()) {
        targetParagraphFirstHalf.paragraphStyle = firstNewParagraph.paragraphStyle
        targetParagraphFirstHalf.type = firstNewParagraph.type
    }

    if (newParagraphs.size == 1) {
        // Before position + Pasted Content + After Position

        firstNewParagraph.updateChildrenParagraph(targetParagraphFirstHalf)
        targetParagraphSecondHalf.updateChildrenParagraph(targetParagraphFirstHalf)
        targetParagraphFirstHalf.children.addAll(firstNewParagraph.children)
        targetParagraphFirstHalf.children.addAll(targetParagraphSecondHalf.children)
        targetParagraphFirstHalf.removeEmptyChildren()
    } else {
        // Before position + First pasted paragraph
        // Pasted paragraphs between first and last
        // Last pasted paragraph + After position

        val lastNewParagraph = newParagraphs.last()

        // Before position + First pasted paragraph
        firstNewParagraph.updateChildrenParagraph(targetParagraphFirstHalf)
        targetParagraphFirstHalf.children.addAll(firstNewParagraph.children)
        targetParagraphFirstHalf.removeEmptyChildren()

        // Pasted paragraphs between first and last
        if (newParagraphs.size >= 3) {
            val middleParagraphs = newParagraphs.subList(1, newParagraphs.size - 1)
            richParagraphList.addAll(paragraphIndex + 1, middleParagraphs)
        }

        // Last pasted paragraph + After position
        targetParagraphSecondHalf.updateChildrenParagraph(lastNewParagraph)
        lastNewParagraph.children.addAll(targetParagraphSecondHalf.children)
        lastNewParagraph.removeEmptyChildren()
        richParagraphList.add(paragraphIndex + newParagraphs.size - 1, lastNewParagraph)
    }

    // Update the state
    updateRichParagraphList()
}


internal fun RichTextState.updateParagraphType(
    paragraph: RichParagraph,
    newType: ParagraphType,
) {
    updateTextFieldValue(
        newTextFieldValue = updateParagraphType(
            paragraph = paragraph,
            newType = newType,
            textFieldValue = this.textFieldValue,
        )
    )
}

internal fun RichTextState.updateParagraphType(
    paragraph: RichParagraph,
    newType: ParagraphType,
    textFieldValue: TextFieldValue,
): TextFieldValue {
    val selection = textFieldValue.selection
    val paragraphOldStartTextLength = paragraph.type.startText.length
    val textFieldValueDiff = this.textFieldValue.text.length - textFieldValue.text.length
    val firstNonEmptyChildIndex = paragraph.getFirstNonEmptyChild()?.textRange?.min?.let {
        if (it >= selection.min) it - textFieldValueDiff
        else it
    }
    val paragraphFirstChildStartIndex = firstNonEmptyChildIndex ?: selection.min

    paragraph.type = newType

    // If the paragraph type start text length didn't change, we don't need to update the text field value
    if (paragraphOldStartTextLength == newType.startText.length) return textFieldValue

    val beforeText = textFieldValue.text.substring(0, paragraphFirstChildStartIndex - paragraphOldStartTextLength)
    val afterText = textFieldValue.text.substring(paragraphFirstChildStartIndex)

    val newSelectionMin =
        if (selection.min > paragraphFirstChildStartIndex) selection.min + newType.startText.length - paragraphOldStartTextLength
        else if (selection.min == paragraphFirstChildStartIndex) paragraphFirstChildStartIndex + newType.startText.length - paragraphOldStartTextLength
        else selection.min
    val newSelectionMax =
        if (selection.max > paragraphFirstChildStartIndex) selection.max + newType.startText.length - paragraphOldStartTextLength
        else if (selection.max == paragraphFirstChildStartIndex) paragraphFirstChildStartIndex + newType.startText.length - paragraphOldStartTextLength
        else selection.max

    return textFieldValue.copy(
        text = beforeText + newType.startText + afterText,
        selection = TextRange(
            newSelectionMin,
            newSelectionMax,
        ),
    )
}


/**
 * Updates the [RichTextState] with the given [newRichParagraphList].
 * The [RichTextState] will be updated with the given [newRichParagraphList] and the [annotatedString] will be updated.
 *
 * @param newRichParagraphList The [RichParagraph]s to update the [RichTextState] with.
 */
internal fun RichTextState.updateRichParagraphList(newRichParagraphList: List<RichParagraph>) {
    richParagraphList.clear()
    richParagraphList.addAll(newRichParagraphList)
    updateRichParagraphList()
}

@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.updateRichParagraphList() {
    if (richParagraphList.isEmpty())
        richParagraphList.add(RichParagraph())

    val beforeTextLength = annotatedString.text.length

    val newStyledRichSpanList = mutableListOf<RichSpan>()

    usedInlineContentMapKeys.clear()

    annotatedString = buildAnnotatedString {
        var index = 0
        richParagraphList.fastForEachIndexed { i, richParagraph ->
            withStyle(richParagraph.paragraphStyle.merge(richParagraph.type.getStyle(config))) {
                withStyle(
                    richParagraph.getStartTextSpanStyle() ?: RichSpanStyle.DefaultSpanStyle
                ) {
                    append(richParagraph.type.startText)
                }

                val richParagraphStartTextLength = richParagraph.type.startText.length
                richParagraph.type.startRichSpan.textRange = TextRange(index, index + richParagraphStartTextLength)
                index += richParagraphStartTextLength
                withStyle(RichSpanStyle.DefaultSpanStyle) {
                    index = append(
                        state = this@updateRichParagraphList,
                        richSpanList = richParagraph.children,
                        startIndex = index,
                        onStyledRichSpan = {
                            newStyledRichSpanList.add(it)
                        },
                    )

                    if (!singleParagraphMode) {
                        if (i != richParagraphList.lastIndex) {
                            append(' ')
                            index++
                        }
                    }
                }
            }
        }
    }

    inlineContentMap.keys.forEach { key ->
        if (key !in usedInlineContentMapKeys) {
            inlineContentMap.remove(key)
        }
    }

    val selectionIndex =
        (textFieldValue.selection.min + (annotatedString.text.length - beforeTextLength))
            .coerceIn(0, annotatedString.text.length)

    styledRichSpanList.clear()
    textFieldValue = TextFieldValue(
        text = annotatedString.text,
        selection = TextRange(selectionIndex),
    )
    visualTransformation = VisualTransformation { _ ->
        TransformedText(
            text = annotatedString,
            offsetMapping = OffsetMapping.Identity
        )
    }
    styledRichSpanList.addAll(newStyledRichSpanList)

    // Clear un-applied styles
    initState()

    // Update current span style
    updateCurrentSpanStyle()

    // Update current paragraph style
    updateCurrentParagraphStyle()

    // Check paragraphs type
    checkParagraphsType()
}



private fun RichTextState.checkParagraphsType() {
    tempTextFieldValue = textFieldValue

    // Todo: It's not the best way to set start text span style, try to set it from parser
    var orderedListStartTextSpanStyle = SpanStyle()

    val levelNumberMap = hashMapOf<Int, Int>()

    richParagraphList.fastForEachIndexed { index, richParagraph ->
        val type = richParagraph.type

        if (type is ConfigurableListLevel) {
            // Clear the completed list levels
            levelNumberMap.keys.toList().fastForEach { level ->
                if (level > type.level)
                    levelNumberMap.remove(level)
            }
        } else {
            // Clear the map if the current paragraph is not a list
            levelNumberMap.clear()
        }

        // Remove current list level from map if the current paragraph is an unordered list
        if (type is UnorderedList)
            levelNumberMap.remove(type.level)

        if (type is OrderedList) {
            val orderedListNumber =
                levelNumberMap[type.level]
                    ?.plus(1)
                    ?: 1

            levelNumberMap[type.level] = orderedListNumber

            if (orderedListNumber == 1)
                orderedListStartTextSpanStyle =
                    richParagraph.getFirstNonEmptyChild()?.spanStyle ?: SpanStyle()

            tempTextFieldValue = updateParagraphType(
                paragraph = richParagraph,
                newType = OrderedList(
                    number = orderedListNumber,
                    config = config,
                    startTextWidth = type.startTextWidth,
                    initialLevel = type.level
                ),
                textFieldValue = tempTextFieldValue,
            )

            type.number = orderedListNumber
        } else {
            orderedListStartTextSpanStyle = SpanStyle()
        }
    }

    updateTextFieldValue()
}


/**
 * Returns the [RichParagraph] that contains the given [textIndex].
 * If no [RichParagraph] contains the given [textIndex], null is returned.
 *
 * @param textIndex The text index to search for.
 * @return The [RichParagraph] that contains the given [textIndex], or null if no such [RichParagraph] exists.
 */
internal fun RichTextState.getRichParagraphByTextIndex(textIndex: Int): RichParagraph? {
    if (singleParagraphMode) return richParagraphList.firstOrNull()
    if (textIndex < 0) return richParagraphList.firstOrNull()

    var index = 0
    var paragraphIndex = -1
    return richParagraphList.fastFirstOrNull { richParagraphStyle ->
        paragraphIndex++
        val result = richParagraphStyle.getRichSpanByTextIndex(
            paragraphIndex = paragraphIndex,
            textIndex = textIndex,
            offset = index,
        )
        index = result.first
        result.second != null
    }
}

/**
 * Returns a list of [RichParagraph]s that contains at least a part of the given [searchTextRange].
 * If no [RichParagraph] contains at least a part of the given [searchTextRange], an empty list is returned.
 *
 * @param searchTextRange The [TextRange] to search for.
 * @return A list of [RichParagraph]s that contains a part of the given [searchTextRange],
 * or an empty list if no such [RichParagraph] exists.
 */
internal fun RichTextState.getRichParagraphListByTextRange(searchTextRange: TextRange): List<RichParagraph> {
    if (singleParagraphMode) return richParagraphList.toList()

    var index = 0
    val richParagraphList = mutableListOf<RichParagraph>()
    this.richParagraphList.fastForEachIndexed { paragraphIndex, richParagraphStyle ->
        val result = richParagraphStyle.getRichSpanListByTextRange(
            paragraphIndex = paragraphIndex,
            searchTextRange = searchTextRange,
            offset = index,
        )
        if (result.second.isNotEmpty())
            richParagraphList.add(richParagraphStyle)
        index = result.first
    }
    return richParagraphList
}


/**
 * Slice [RichParagraph] by [startIndex] and [richSpan] that contains [startIndex].
 * The passed [RichParagraph] will be modified, containing only the text before [startIndex].
 * And the new [RichParagraph] will be returned, containing the text after [startIndex].
 *
 * @param startIndex The start index of the slice.
 * @param richSpan The [RichSpan] that contains [startIndex].
 * @return The new [RichParagraph].
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichParagraph.slice(
    startIndex: Int,
    richSpan: RichSpan,
    removeSliceIndex: Boolean,
): RichParagraph {
    val newRichParagraph = RichParagraph(
        paragraphStyle = paragraphStyle,
        type = type.getNextParagraphType(),
    )

    var previousRichSpan: RichSpan
    var currentRichSpan: RichSpan = richSpan

    val textStartIndex =
        if (startIndex == type.startRichSpan.textRange.min)
            startIndex - richSpan.textRange.min + type.startRichSpan.text.length
        else
            startIndex - richSpan.textRange.min

    newRichParagraph.type.startRichSpan.paragraph = newRichParagraph
    newRichParagraph.type.startRichSpan.textRange = TextRange(
        0,
        newRichParagraph.type.startRichSpan.text.length
    )

    val beforeText =
        richSpan.text.substring(
            startIndex = 0,
            endIndex = textStartIndex
                .coerceIn(0, richSpan.text.length)
        )
    val afterTextStartIndex =
        if (removeSliceIndex)
            textStartIndex + 1
        else
            textStartIndex
    val afterText =
        richSpan.text.substring(
            startIndex = afterTextStartIndex
                .coerceIn(0, richSpan.text.length)
        )

    richSpan.text = beforeText
    richSpan.textRange = TextRange(
        richSpan.textRange.min,
        richSpan.textRange.min + beforeText.length
    )

    // We don't copy the current rich span style to the new rich span
    val newRichSpan = RichSpan(
        paragraph = newRichParagraph,
        parent = null,
        text = afterText,
        textRange = TextRange(
            startIndex,
            startIndex + afterText.length
        ),
        spanStyle = richSpan.fullSpanStyle,
    )

    newRichParagraph.children.add(newRichSpan)

    for (i in richSpan.children.lastIndex downTo 0) {
        val childRichSpan = richSpan.children[i]
        richSpan.children.removeAt(i)
        childRichSpan.parent = newRichSpan
        childRichSpan.paragraph = newRichParagraph
        newRichSpan.children.add(childRichSpan)
    }

    while (true) {
        previousRichSpan = currentRichSpan
        currentRichSpan = currentRichSpan.parent ?: break

        val index = currentRichSpan.children.indexOf(previousRichSpan)
        if (index in 0 until currentRichSpan.children.lastIndex) {
            ((index + 1)..currentRichSpan.children.lastIndex).forEach {
                val childRichSpan = currentRichSpan.children[it]
                childRichSpan.spanStyle = childRichSpan.fullSpanStyle
                childRichSpan.parent = null
                childRichSpan.paragraph = newRichParagraph
                newRichParagraph.children.add(childRichSpan)
            }
            currentRichSpan.children.removeRange(index + 1, currentRichSpan.children.size)
        }
    }

    val index = richSpan.paragraph.children.indexOf(previousRichSpan)
    if (index in 0 until richSpan.paragraph.children.lastIndex) {
        ((index + 1)..richSpan.paragraph.children.lastIndex).forEach {
            val childRichSpan = richSpan.paragraph.children[it]
            childRichSpan.spanStyle = childRichSpan.fullSpanStyle
            childRichSpan.parent = null
            childRichSpan.paragraph = newRichParagraph
            newRichParagraph.children.add(childRichSpan)
        }
        richSpan.paragraph.children.removeRange(index + 1, richSpan.paragraph.children.size)
    }

    return newRichParagraph
}

/**
 * Internal helper for testing
 */
internal fun RichTextState.printParagraphs() {
    richParagraphList.fastForEachIndexed { i, richParagraph ->
        println("Paragraph $i: $richParagraph")
    }
}